Hoisting is JavaScript's behavior where variable and function declarations are conceptually moved to the top of their containing scope during compilation, implemented internally by the engine creating environment records and allocating memory for declarations before code execution begins.
Hoisting is not a physical movement of code but a mental model for understanding how the JavaScript engine processes declarations. Internally, the engine implements hoisting through a two-phase process when creating execution contexts: first, during the compilation phase, it scans for all declarations and sets up environment records with appropriate bindings; then, during the execution phase, it runs the code line by line. This separation is why you can access variables and functions before their declaration in the source code, though with different behaviors depending on how they were declared.
Creation Phase (Memory Allocation): When entering a new scope (global, function, or block), the engine performs a pre-scan of the code to identify all declarations. It allocates memory for them in the environment record and sets up initial values .
Execution Phase: After the environment is prepared, the engine begins executing the code line by line, performing assignments and evaluating expressions .
Temporal Dead Zone (TDZ): For let and const, the engine hoists the declaration but does NOT initialize it, leaving it in an uninitialized state until the actual declaration line is reached during execution .
Environment Records: Each scope has an associated Environment Record object that stores bindings. The creation phase populates this record with all declared identifiers .
The key insight is that hoisting is an artifact of how the engine prepares environments before running code. The parser identifies all declarations in a scope and creates slots for them in the environment record. This preparatory step happens before any code executes, which is why declarations appear to be 'hoisted'—they exist in memory from the moment execution starts, even if the line declaring them hasn't been reached yet.
var declarations: Hoisted and automatically initialized with undefined. This is why accessing a var before declaration is safe but yields undefined .
let and const declarations: Hoisted but NOT initialized. They remain in the Temporal Dead Zone (TDZ) from the start of the block until the declaration line is executed. Accessing them during TDZ throws a ReferenceError .
function declarations: Fully hoisted—both the name and the function body are initialized immediately. You can call a function declared this way before its line in the code .
function expressions: Treated like variable assignments. var fn = function() {} is hoisted as undefined (var behavior). let fn = function() {} is hoisted but uninitialized (TDZ) .
class declarations: Hoisted but not initialized, similar to let. Accessing a class before declaration throws a TDZ error .
import declarations: Hoisted and initialized (they happen before the rest of the code runs) .
The Temporal Dead Zone is an important refinement to hoisting introduced with ES6. Prior to let and const, all hoisting behaved like var—initialized with undefined. But this could mask bugs, as accessing variables before assignment would silently work with undefined. The TDZ makes such accesses throw errors, forcing developers to declare variables before use while still allowing the engine to know about the variable from the start of the scope for optimization purposes.
Scope analysis during parsing: The parser identifies all declarations in a scope and allocates fixed slots in the environment record, enabling fast variable access later .
Function hoisting optimization: Function declarations are pre-created as function objects during parsing, so calling them doesn't require runtime creation .
TDZ checks: The engine inserts implicit runtime checks before any access to lexical bindings that might be in the TDZ, which adds a tiny overhead .
Block scoping implementation: Each block with let/const creates a new lexical environment record, chained to the outer environment, maintaining proper TDZ semantics .
For developers, understanding hoisting helps avoid common bugs and write more predictable code. The modern best practice is to declare variables at the top of their scope (to make the hoisting explicit) and prefer const and let over var to benefit from TDZ protection. But internally, regardless of where you put your declarations, the engine always sees them first—hoisting is simply how JavaScript's lexical scoping is implemented under the hood.